CtrlK
BlogDocsLog inGet started
Tessl Logo

TypeScript Development

This skill should be used when the user asks to "write TypeScript code", "create a TypeScript module", "define TypeScript types", "add type annotations", "use generics", "handle errors in TypeScript", "set up tsconfig", "organize TypeScript project", or when writing any TypeScript code that is not tied to a specific library or framework. Covers type system, strict mode, naming conventions, error handling, async patterns, and project structure.

Invalid
This skill can't be scored yet
Validation errors are blocking scoring. Review and fix them to unlock Quality, Impact and Security scores. See what needs fixing →
SKILL.md
Quality
Evals
Security

TypeScript Development

This skill covers modern TypeScript best practices for writing clean, type-safe code. It focuses on the language itself — no library or framework specifics. Apply these conventions to all TypeScript code in the project.

Strict Mode

Always enable strict: true in tsconfig.json. Never disable individual strict flags. This is non-negotiable — it catches entire categories of bugs at compile time.

Key strict behaviors:

  • null and undefined are distinct types (no implicit null)
  • Every value must have a known type (no implicit any)
  • Catch clause variables are unknown, not any

Naming Conventions

ConstructConventionExample
Variables, functionscamelCasegetUserName, isActive
ClassesPascalCaseUserService, HttpClient
InterfacesPascalCase (no I prefix)User, not IUser
Type aliasesPascalCaseApiResponse, EventMap
ConstantscamelCase or UPPER_SNAKEmaxRetries or MAX_RETRIES
Enum-like objectsPascalCase key, camelCase/string valuesStatus.Active
Generic parametersSingle uppercase or descriptiveT, TResult, K extends keyof T
File nameskebab-caseuser-service.ts, api-client.ts
Boolean variablesPrefix with is, has, can, shouldisValid, hasPermission

Type Annotations

When to annotate explicitly

  • Function parameters — always
  • Function return types — always for exported functions, optional for local functions
  • Class properties — always
  • Variables — only when the type cannot be inferred
// Parameters and return: always annotate
function calculateTotal(items: LineItem[], taxRate: number): number {
  return items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate);
}

// Variable: skip annotation when inferred
const total = calculateTotal(items, 0.1); // inferred as number

// Variable: annotate when not obvious
const cache: Map<string, User> = new Map();

Prefer interfaces for object shapes

// Prefer interface for object shapes
interface User {
  id: string;
  name: string;
  email: string;
}

// Use type alias for unions, intersections, mapped types
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
type StringKeys<T> = Extract<keyof T, string>;

Avoid any

Use unknown instead of any for values of uncertain type. Narrow with type guards before use:

// Bad
function parse(input: any): string { return input.name; }

// Good
function parse(input: unknown): string {
  if (typeof input === "object" && input !== null && "name" in input) {
    return String((input as { name: unknown }).name);
  }
  throw new Error("Invalid input");
}

Acceptable uses of any: Only when interfacing with untyped external code and a proper type cannot be defined. Always add a comment explaining why.

Discriminated Unions

Model state variants with a shared literal discriminant:

type LoadingState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

Always include an exhaustive check using never:

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

function render<T>(state: LoadingState<T>): string {
  switch (state.status) {
    case "idle": return "Ready";
    case "loading": return "Loading...";
    case "success": return String(state.data);
    case "error": return state.error.message;
    default: return assertNever(state);
  }
}

Error Handling

Catch unknown errors

try {
  await riskyOperation();
} catch (error: unknown) {
  if (error instanceof AppError) {
    handleAppError(error);
  } else if (error instanceof Error) {
    handleGenericError(error);
  } else {
    handleUnknown(String(error));
  }
}

Custom error classes

class AppError extends Error {
  constructor(
    message: string,
    readonly code: string,
    readonly statusCode: number = 500,
  ) {
    super(message);
    this.name = "AppError";
  }
}

Result type over exceptions

For expected failure paths, prefer a typed Result over throwing:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

Const Objects Over Enums

Prefer as const objects over TypeScript enum:

const Status = {
  Active: "active",
  Inactive: "inactive",
  Pending: "pending",
} as const;

type Status = (typeof Status)[keyof typeof Status];

Why: No runtime code emitted, better tree-shaking, interoperates with plain strings.

Async Code

  • Always specify Promise<T> return types on async functions
  • Use Promise.all for independent concurrent operations
  • Use Promise.allSettled when partial failure is acceptable
  • Never use void for async function returns — use Promise<void>
async function fetchUserData(id: string): Promise<UserData> {
  const [profile, orders] = await Promise.all([
    fetchProfile(id),
    fetchOrders(id),
  ]);
  return { profile, orders };
}

Immutability

  • Use readonly on properties that should not change after initialization
  • Use readonly T[] (or ReadonlyArray<T>) for array parameters that should not be mutated
  • Use as const for literal objects and arrays that should be fully immutable
  • Prefer spreading over mutation: { ...obj, key: newValue } over obj.key = newValue

Type-Only Imports

Use import type for imports used only as types:

import type { User } from "./models.js";
import { createUser } from "./models.js";

This prevents circular dependency issues and ensures types are erased at compile time.

ESM Import Rule

When using "type": "module" in package.json with "module": "Node16", all relative imports must include the .js extension — even in .ts source files:

import { helper } from "./utils.js";   // Correct
import { helper } from "./utils";      // Wrong — fails at runtime

Quick Reference: Common Mistakes

MistakeFix
Using anyUse unknown and narrow with type guards
Missing return type on exportsAdd explicit return type annotation
enum for string constantsUse as const object + derived union type
Mutable function parametersMark arrays/objects as readonly
Bare catch (error)Use catch (error: unknown) and narrow
Missing .js in ESM importsAdd .js extension to all relative imports
strict: false in tsconfigAlways use strict: true
I prefix on interfacesDrop the prefix: User, not IUser
Optional props for distinct statesUse discriminated unions
Type assertions (as T)Prefer type guards and narrowing

Additional Resources

Reference Files

For detailed type system features and advanced patterns, consult:

  • references/type-system.md — Generics, utility types, conditional types, mapped types, template literal types, type guards, discriminated unions, branded types, satisfies operator, const assertions, declaration merging
  • references/patterns.md — Immutability patterns, error handling (Result type, custom errors), async patterns (generators, concurrency), builder pattern, type-safe event emitter, overloaded functions, module patterns, enum alternatives, assertion functions, narrowing patterns
  • references/project-structure.md — tsconfig.json essentials (strict mode flags, module config, safety flags), ESM/CJS setup, directory layout, organizing types, barrel exports, declaration files, import organization, path aliases, gitignore
Repository
provectus/awos-recruitment
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.